package com.example.redisdistribute.service.impl;

import com.example.redisdistribute.service.IRedisDistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * redis 分布式锁
 *
 * @ClassName IRedisDistributedLockImpl
 * @Author lixingxing
 * @Date 2021/08/11 下午 14:40
 * @Version 1.0
 */
@Slf4j
@Component
public class IRedisDistributedLockImpl implements IRedisDistributedLock {

    /**
     * redis前缀
     */
    private static final String PREFIX = "Lock:";

    /**
     * 保存锁的value
     */
    private ThreadLocal<String> threadLocal = new ThreadLocal<>();

    /**
     * 字符编码
     */
    private static final Charset utf8 = Charset.forName("UTF-8");

    /**
     * 释放锁脚本，lua原子操作
     */
    private static final String UNLOCK_LUA;
    static {
        StringBuilder sb = new StringBuilder();
        sb.append(" if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append(" then ");
        sb.append("     return redis.call(\"del\",KEYS[1]) ");
        sb.append(" else ");
        sb.append("     return 0 ");
        sb.append(" end ");
        UNLOCK_LUA = sb.toString();
    }

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public boolean lock(String key, Long requireTimeOut, Long lockTimeOut, Integer retries) {
        // 可重入锁判断
        String originValue = threadLocal.get();
        if (!StringUtils.hasText(originValue) && isReentrantLock(key, originValue)) {
            return true;
        }
        String value = UUID.randomUUID().toString();
        Long end = System.currentTimeMillis() + requireTimeOut;
        int retryTimes = 1;

        try {
            while (System.currentTimeMillis() < end) {
                if (retryTimes > retries) {
                    log.error(" require lock failed,retry times [{}]", retries);
                    return false;
                }
                if (setNX(wrapLockKey(key), value, lockTimeOut)) {
                    threadLocal.set(value);
                    return true;
                }
                // 休眠10ms
                Thread.sleep(10);
                retryTimes++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }


    private Boolean setNX(String key, String value, Long lockTimeOut) {
        // List设置的lua的Keys
        List<String> keyList = new ArrayList<>();
        keyList.add(key);
        Boolean executeResult = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
            Boolean result = connection.set(key.getBytes(utf8),
                    value.getBytes(utf8),
                    Expiration.milliseconds(lockTimeOut),
                    RedisStringCommands.SetOption.SET_IF_ABSENT);
            return result;
        });
        return executeResult;
    }

    /**
     * key
     * @param key
     * @return
     */
    private String wrapLockKey(String key) {
        return PREFIX + key;
    }

    /**
     * 是否为重入锁
     * @param key
     * @param originValue
     * @return
     */
    private boolean isReentrantLock(String key, String originValue) {
        String value = (String) redisTemplate.opsForValue().get(key);
        return value != null && originValue.equals(value);
    }

    /**
     * 解锁
     * @param key
     * @return
     */
    @Override
    public boolean unLock(String key) {
        String originValue = threadLocal.get();
        if (!StringUtils.hasText(originValue)) {
            return false;
        }
        Boolean executeResult = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
            Boolean result = connection.eval(
                    UNLOCK_LUA.getBytes(utf8),
                    ReturnType.BOOLEAN,
                    1,
                    wrapLockKey(key).getBytes(utf8),
                    originValue.getBytes(utf8)
            );
            return result;
        });
        return executeResult;
    }
}
